﻿/* 
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See the LICENSE file in the project root for full license information.
 */


import { TPM_CC, TPM_RC, TPM_RH, TPM_ST, TPM_HANDLE } from "./TpmTypes.js";
import { TpmError, TpmDevice, TpmTcpDevice, TpmTbsDevice, TpmLinuxDevice } from "./TpmDevice.js";
import { TpmBuffer, TpmMarshaller } from "./TpmMarshaller.js";
import { Session } from "./Tss.js";
import { Tpm } from "./Tpm.js";
import { ReqStructure, RespStructure } from "./TpmStructure.js";

export { TpmError };

export class TpmBase
{
    //
    // TPM object state
    //

    private device: TpmDevice;

    /** Response code returned by the last executed command */
    private _lastResponseCode: TPM_RC = TPM_RC.NOT_USED;

    /** Error object (may be null) generated during the last TPM command execution */
    private _lastError: TpmError = null;

    //
    // Per-command state
    //

    /** TPM sessions associated with the next command. */
    private sessions: Session[] = null;

    /** Controls whether exceptions are enabled */
	private exceptionsEnabled: boolean = false;

    /** Suppresses exceptions in response to the next command failure, when exceptions are enabled */
	private errorsAllowed: boolean = true;

    //
    // Scratch members
    //
    private cmdCode: TPM_CC;
    private sessTag: TPM_ST;


    /**
     * Checks whether the response code is generated by the TSS.JS (i.e. is an extension
     * to the TPM 2.0 spec).
     *
     * @param code Response code returned by TSS.JS
     * @return true if the response code was generated in the communication channel between the app and the TPM
     */
    private static isCommMediumError(code: TPM_RC): boolean
    {
        // TBS or TPMSim protocol error
        return (code & 0xFFFF0000) == 0x80280000;
    }

    private static cleanResponseCode(rawResponse: TPM_RC): TPM_RC
    {
        if (this.isCommMediumError(rawResponse))
            return rawResponse;

        let mask: number = (rawResponse & TPM_RC.RC_FMT1) != 0
                         ? TPM_RC.RC_FMT1 | 0x3F : TPM_RC.RC_WARN | TPM_RC.RC_VER1 | 0x7F;
        return rawResponse & mask;
    }


    constructor(useSimulator: boolean = false,
                host: string = '127.0.0.1', port: number = 2321)
    {
        this.device = useSimulator ? new TpmTcpDevice(host, port)
                    : process.platform == 'win32' ? new TpmTbsDevice()
                                                  : new TpmLinuxDevice();
    }
    
    public connect(continuation: () => void)
    {
        let err = this.device.connect(continuation);
        if (err && this.device instanceof TpmLinuxDevice)
        {
            // It is possible that a user mode TRM from tpm2-tools is running
            this.device = new TpmTcpDevice('127.0.0.1', 2323, true);
            this.device.connect(continuation);
        }
    }

    public close(): void
    {
        this.device.close();
        this.device = null;
    }

    get lastResponseCode(): TPM_RC
    {
        return this._lastResponseCode;
    }

    get lastError(): TpmError { return this._lastError; }

	/**
	 * For the next TPM command invocation, errors will not cause an exception to be thrown
	 * (use _lastCommandSucceeded or _getLastResponseCode() to check for an error)
	 * 
	 * @return The same object (to allow modifier chaining)
	 */
	public allowErrors(): Tpm
	{
		this.errorsAllowed = true;
		return <Tpm><Object>this;
	}
	
	/**
	 * When exceptions are enabled, errors reported by the TPM or occurred in the TSS (e.g. during 
     * an attempt to communicate with the TPM) will result in throwing an exception of TpmError type.
	 * It will still be possible to use _lastCommandSucceeded(), _getLastResponseCode() methods and
     * lastError property to check for an error after the exception is intercepted.
     * Note that in contrast to allowErrors() this method affects all subsequent commands. 
	 */
    public enableExceptions(enable: boolean = true): void
	{
		this.exceptionsEnabled = enable;
        this.errorsAllowed = !enable;
	}

	/**
	 * Specifies a single session handle to use with the next command 
	 * 
	 * @param hh List of up to 3 session handles 
	 * @return This TPM object
	 */
    public withSession(sess: Session): Tpm
	{
		this.sessions = new Array<Session>(sess);
		return <Tpm><Object>this;
	}

	/**
	 * Specifies the session handles to use with the next command 
	 * 
	 * @param hh List of up to 3 session handles 
	 * @return This TPM object
	 */
    public withSessions(...sess: Session[]): Tpm
	{
		this.sessions = new Array<Session>(...sess);
		return <Tpm><Object>this;
	}

    private ResponseHandler: (resp: TpmBuffer) => void;
    private CmdBuf: TpmBuffer;

    private InterimResponseHandler (err: TpmError, respBuf: Buffer)
    {
        this._lastError = err;
        if (err)
            setImmediate(this.ResponseHandler.bind(this), null);
        else
        {
            let rc: TPM_RC = respBuf.readUInt32BE(6);
            if (rc == TPM_RC.RETRY)
                this.device.dispatchCommand(this.CmdBuf.buffer, this.InterimResponseHandler.bind(this));
            else
                setImmediate(this.ResponseHandler.bind(this), new TpmBuffer(respBuf));
        }
    }

    protected dispatchCommand(
        cmdCode: TPM_CC,
        req: ReqStructure,
        responseHandler: (resp: TpmBuffer) => void
    ): void
    {
        let handles = req.getHandles();
        let numAuthHandles = req.numAuthHandles();
        let cmdBuf = new TpmBuffer();

        this.cmdCode = cmdCode;
        this.sessTag = numAuthHandles > 0 ? TPM_ST.SESSIONS : TPM_ST.NO_SESSIONS;

        // Create command buffer header
        cmdBuf.writeShort(this.sessTag);
        cmdBuf.writeInt(0); // to be filled in later
        cmdBuf.writeInt(cmdCode);

        // Marshal handles, if any
        if (handles != null)
        {
            for (let h of handles)
            {
                if (h == null)
                    cmdBuf.writeInt(TPM_RH.NULL);
                else
                    h.toTpm(cmdBuf);
            }
        }

        // Marshal auth sessions, if any
        if (numAuthHandles > 0)
        {
            // If the caller has not provided a session for a handle that requires authorization,
            // a password session is automatically created.
            if (this.sessions == null)
                this.sessions = new Array<Session>(numAuthHandles);
            else if (this.sessions.length < numAuthHandles)
                this.sessions = this.sessions.concat(new Array<Session>(numAuthHandles - this.sessions.length));

            for (let i: number = 0; i < numAuthHandles; ++i)
            {
                if (this.sessions[i] == null)
                    this.sessions[i] = Session.Pw();
            }

            // We do not know the size of the authorization area yet.
            // Remember the place to marshal it, ...
            let authSizePos = cmdBuf.curPos;
            // ... and marshal a placeholder 0 value for now.
            cmdBuf.writeInt(0);

            for (let sess of this.sessions)
                sess.SessIn.toTpm(cmdBuf);

            cmdBuf.writeNumAtPos(cmdBuf.curPos - authSizePos - 4, authSizePos);
        }
        this.sessions = null;

        // Marshal command parameters
        req.toTpm(cmdBuf);

        // Fill in command buffer size in the command header
        cmdBuf.writeNumAtPos(cmdBuf.curPos, 2);
        cmdBuf.trim();
        this.ResponseHandler = responseHandler;
        this.CmdBuf = cmdBuf;
        this.device.dispatchCommand(cmdBuf.buffer, this.InterimResponseHandler.bind(this));
    } // dispatchCommand()

    protected generateErrorResponse(rc: TPM_RC): TpmBuffer
    {
        let respBuf = new TpmBuffer(10);
        respBuf.writeShort(TPM_ST.NO_SESSIONS);
        respBuf.writeInt(10);
        respBuf.writeInt(rc);
        return respBuf;
    }

    protected generateError(respCode: TPM_RC, errMsg: string, errorsAllowed: boolean): undefined
    {
        this._lastError = new TpmError(respCode, TPM_CC[this.cmdCode], errMsg);
        if (this.exceptionsEnabled && !errorsAllowed)
            throw this._lastError;
        return null;
    }

    // Returns pair [response parameters size, error if any]
    protected processResponse<T extends RespStructure>(respBuf: TpmBuffer, respType?: {new(): T}): T
    {
        if (this.lastError)
            return null;

        let errorsAllowed = this.errorsAllowed;
        this.errorsAllowed = !this.exceptionsEnabled;

        if (respBuf.size < 10)
        {
            return this.generateError(TPM_RC.TSS_RESP_BUF_TOO_SHORT, 
                            'Response buffer is too short: ' + respBuf.size, errorsAllowed);
        }

        if (respBuf.curPos != 0)
            throw new Error("Response buffer reading position is not properly initialized");

        let tag: TPM_ST = respBuf.readShort();
        let respSize: number = respBuf.readInt();
        let rc: TPM_RC = respBuf.readInt();

        this._lastResponseCode = TpmBase.cleanResponseCode(rc);

        if (rc == TPM_RC.SUCCESS && tag != this.sessTag ||
            rc != TPM_RC.SUCCESS && tag != TPM_ST.NO_SESSIONS)
        {
            return this.generateError(TPM_RC.TSS_RESP_BUF_INVALID_SESSION_TAG,
                            'Invalid session tag in the response buffer', errorsAllowed);
        }

        if (this._lastResponseCode != TPM_RC.SUCCESS)
        {
            return this.generateError(this._lastResponseCode, 
                        `TPM command {${TPM_CC[this.cmdCode]}} failed with response code {${TPM_RC[rc]}}`,
                        errorsAllowed);
        }

        if (!respType)
            return null;    // No values are expected to be returned by the TPM

        let resp: T = new respType();

        // Get the handles
        if (resp.numHandles() > 0)
            resp.setHandle(TPM_HANDLE.fromTpm(respBuf));

        // If a response session is present, response buffer contains a field specifying the size of response parameters
        let respParamsSize = tag == TPM_ST.SESSIONS ? respBuf.readInt()
                                                    : respBuf.size - respBuf.curPos;

        let paramStart = respBuf.curPos;
        resp.initFromTpm(respBuf);

        if (respParamsSize != respBuf.curPos - paramStart)
            return this.generateError(TPM_RC.TSS_RESP_BUF_INVALID_SIZE, 
                        `Inconsistent TPM response params size: expected ${respParamsSize}, actual ${respBuf.curPos - paramStart}`,
                        errorsAllowed);

        return resp;
    } // processResponse()

}; // class TpmBase
